#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         RandomizedSection.py
#
# Copyright:    Copyright (c) 2004, Faulkner Technologies
#
# Author:       Brian Oliver
#
# Description:  A RandomizedSection is a container for Components
#               within a Model that randomly displays 
#               (in a controller manner ;) it's children during
#               the assessment process, the purpose being to 
#               enable quiz-like pages of questions.
#
#               Like Sections, RandomizedSections that are immediate 
#               children of Models are commonly refered to as 
#               "Page Sections" or "Pages" as when rendered for an 
#               assessment editor, they are treated as web pages.
#####################################################################
#####################################################################
## Knowledge Engine Library Imports
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.Core import *
from Products.KnowledgeEngine.Component import Component, ComponentInfo
from Products.KnowledgeEngine.Components.Section.Section import Section, SectionInfo
from Products.KnowledgeEngine.Assessible import Assessible
from Products.KnowledgeEngine.AssessmentComponentState import AssessmentComponentState
from Products.KnowledgeEngine.AssessmentComponentValueTypes import *
from Products.KnowledgeEngine.KERLExtensions import KERLExtensions
from Products.KnowledgeEngine.PropertyDefinition import *
from Products.KnowledgeEngine.PropertyTypes import *
from Products.KnowledgeEngine.PropertyFormats import *
from Products.KnowledgeEngine.PropertyStructures import *
from Products.KnowledgeEngine.EventDefinition import *
#####################################################################
## Zope Library Imports
from Globals import DTMLFile
from Globals import Persistent
from App.ImageFile import ImageFile

#####################################################################
## Python Library Imports
from random import shuffle

#####################################################################
## RandomizedSectionInfo Class Definition
class RandomizedSectionInfo(SectionInfo):
    """
        Defines the class info methods, defining information about
        the class without needing an instance of the class
    """

    #---------------------------------------------------------------#
    
    def getClassName(self):
        """
            Return the class name
        """
        
        return 'RandomizedSection'

    #---------------------------------------------------------------#

    def getDisplayName(self):
        """
            Return a friendly name for the class instances
            (this is used for displaying in lists etc)
        """
    
        return "Randomized Section"
        
    #---------------------------------------------------------------#

    def getIdentityType(self):
        """
            Return the identity type.  
        """
        
        return "randsec"
        
    #---------------------------------------------------------------#
    
    def getClassIcon(self):
        """
            Return the appropriate icon file for the class.
        """
        
        return ImageFile('skins/RandomizedSection_icon.gif', globals())

    #---------------------------------------------------------------#

    def isInstanceOf(self, className):
        """
            Returns if the class is an instance of the specified
            className, or implements the interface specified 
            by the className
        """

        return className == 'Assessible' or className == 'Section' or SectionInfo.isInstanceOf(self, className)

    #---------------------------------------------------------------#

    def isChildPermitted(self, childClassInfo, childInstance = None):
        """
            Returns if the specified child is permitted as a 
            child of the class' instances.
            
            NOTE: This is from this class' instance perspective.
            
            For component containers, this always overrides what the
            return value from childClassInfo.isPermittedInContainer(...)
        """
        
        #DEFAULT RULE: Sections may have Component or Element children
        return childClassInfo.isInstanceOf('Component') or childClassInfo.isInstanceOf('Element') or childClassInfo.isInstanceOf('Event')

    #---------------------------------------------------------------#

    def onConfigureProperties(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register 
            class specific properties using the PropertyReflection interface.
            
            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered
            
            NOTE: This method is called before ClassInfo.onAfterStartup()            
        """

        #initialise the super-class properties for reflection
        SectionInfo.onConfigureProperties(self)
        
        #add the 'nrComponentsToDisplay' property        
        self.addPropertyDefinition(PropertyDefinition(identity="nrComponentsToDisplay", 
                                                      displayName="Components to Display", 
                                                      description="The number of components to randomly choose and display in a random order during an assessment", 
                                                      defaultValue=-1, 
                                                      propertyType=pt_INTEGER, 
                                                      propertyFormat=pf_NATIVE, 
                                                      propertyStructure=ps_ATOMIC, 
                                                      constructorEVAP=evap_NONE, 
                                                      hasEnumeratedValues=true, 
                                                      enumeratedValuesFunction=(lambda context:[('(All Contained)', -1)] + [('%d' % nr, nr) for nr in range(0, len(context.getChildren())+1)])))
    
    #---------------------------------------------------------------#

    def onConfigureEvents(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register 
            class specific events using the EventReflection interface.
            
            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered
            
            NOTE: This method is called before ClassInfo.onAfterStartup()            
        """
                
        #initialise the super-class events for reflection
        SectionInfo.onConfigureEvents(self)

    #---------------------------------------------------------------#

#####################################################################
## RandomizedSection Class Definition
class RandomizedSection(Section, Assessible, KERLExtensions):
    """
        Simplest Component Container
    """

    #---------------------------------------------------------------#

    #TRANSIENT: a ClassInfo implementation instance to provide info about this class
    _CLASSINFO = RandomizedSectionInfo()
 
    #---------------------------------------------------------------#
    # DTML Methods
    index_html = DTMLFile('dtml/index_html', globals())
    simplereport_html = DTMLFile('dtml/simplereport_html', globals())
    scoredreport_html = DTMLFile('dtml/scoredreport_html', globals())
    scored360report_html = DTMLFile('dtml/scored360report_html', globals())
    groupedreport_html = DTMLFile('dtml/groupedreport_html', globals())    
    #csv360report_data = DTMLFile('dtml/csv360report_data', globals())
    
    #---------------------------------------------------------------#
    # Default Attribute Values
    isIndented = 1
   
    #---------------------------------------------------------------#

    def __init__(self, id, identity):
        "Constructor for the Section"        

        Section.__init__(self, id, identity)

    #---------------------------------------------------------------#

    def onAfterInstanciation(self):
        """
            Handle Knowledge Engine specific actions after this object 
            has been loaded into memory.  
            
            This is useful for performing backwards compatibility
        """

        #perform backwards compatibility work
        Section.onAfterInstanciation(self)
    
    #---------------------------------------------------------------#

    def getNrComponentsToDisplay(self):
        """
            Returns the number of components we are required to
            display of the contained children
        """
    
        return self.nrComponentsToDisplay
    
    #---------------------------------------------------------------#

    def getRandomizedChildren(self, assessment = None):
        """
            Return a list of randomly chosen immediate child components    
            in a random order.
            
            SIDE-EFFECT: If an assessment is provided, the
            assessment state is updated for this component
            
            RETURN TYPE: [Component]
        """

        #do we need to display any children at all?
        if self.getNrComponentsToDisplay() == 0:
        
            #do we have an assessment?
            if assessment is not None:
                #set the default assessment state for the assessment (which is [])
                assessment.setComponentState(self.getIdentity(), self.getDefaultAssessmentState(assessment))

            #we return an empty list as there are no components                
            return []
        
        else:
            #create a dictionary of all of the immediate children of this section
            childDictionary = {}
            for child in self.getChildren():
                childDictionary[child.getIdentity()] = child
                
            #get the children we previously randomly selected for the specified assessment (if we've got one)
            if assessment is not None:
                #get the component state from the assessment
                componentState = assessment.getComponentState(self.getIdentity())
            
                #get the list of children ids (a string) "[String]" from the assessment
                #and convert into a standard python list
                storedState = eval(componentState.getValue())
                
                #resolve the child list into a list of component instances that are actual children 
                #of the section
                selectedChildList = [childDictionary[childId] for childId in storedState if childDictionary.has_key(childId)]
                
            else:
                #as we don't have an assessment so we'll have to choose some
                selectedChildList = []
        
            #determine the desired number of children to display
            if self.getNrComponentsToDisplay() == -1:
                nrDesiredChildren = len(childDictionary)
            else:
                nrDesiredChildren = self.getNrComponentsToDisplay()

            #ensure we have enough children in the list
            if len(selectedChildList) < nrDesiredChildren:
                #too few children... have to add some more randomly chosen children
                
                #remove all of the currently selected children from the dictionary
                #(this will leave us with a possible list to select from)
                for child in selectedChildList:
                    if childDictionary.has_key(child.getIdentity()):
                        del childDictionary[child.getIdentity()]
                
                #shuffle the remaining children
                remainingChildren = childDictionary.values()
                shuffle(remainingChildren)
                
                #add the remaining values to the selected list
                selectedChildList = selectedChildList + remainingChildren

            #we may have to chop the list (no ELSE here!)            
            if len(selectedChildList) > nrDesiredChildren:
                #too many children... chop the list to the desired length
                selectedChildList = selectedChildList[0:nrDesiredChildren]
            
            #if we have an assessment, we should save what we've just worked out into the assessment
            if assessment is not None:
                #convert the [Component] into the component persistent state "[String]"
                storedState = repr([child.getIdentity() for child in selectedChildList])
            
                #update the component state
                componentState.setValue(storedState)
                
                #update the assessment
                assessment.setComponentState(self.getIdentity(), componentState)

                #as we've randomised the children, we'd better save the assessment 
                #otherwise we might generate a different set of children if we 
                #don't navigate to the children immediately (to cause a save)
                self.getModel().getAssessmentStorageService().saveAssessment(assessment)
            
            #return the selected list of children
            return selectedChildList
            
    #---------------------------------------------------------------#
        
    def getPromotedChildren(self, assessment = None):
        """
            Return a sequence of promoted children for this Section
            (returns [] if the section isn't promoting children)
            This method is used to determine top-level pages.
            If an assessment is not provided, all possible 
            promoted children must be returned.
        """
        
        #for randomised sections, we return the randomised children (if we are promoting)
        #otherwise we return all of the children
        if assessment is not None and self.getIsPromotingChildren():
        
            #get the randomised children
            children = self.getRandomizedChildren(assessment)            
            
            #now we can return the children            
            return children
            
        else:
            return Section.getPromotedChildren(self, assessment)
    
    #---------------------------------------------------------------#
    
    def getFirstComponentChild(self, assessment = None):
        """
            Returns the first ordered child in the container.
            (optionally using the assessment for
            dynamically controlled children)
            
            RETURNS: None if the aren't any ordered children or if the 
            container isn't ordered
        """
        
        #get the children
        children = self.getRandomizedChildren(assessment)
        
        #are there any children
        if len(children) == 0:            
            return None
        else:   
            return children[0]
    
    #---------------------------------------------------------------#
    
    def getComponentSiblingAfterChild(self, child, assessment = None):
        """
            Returns the child immediately after the specified child
            (optionally using the assessment for
            dynamically controlled children)
            
            RETURNS: None if the aren't any children after the specified
            child (or if the container isn't ordered)
        """
        
        #get the children
        children = self.getRandomizedChildren(assessment)
        
        #remove children that are before the specified child
        while len(children) > 0 and children[0].getIdentity() != child.getIdentity():
            del children[0]
            
        #return the child after the first (specified) child (which is the one we are after)
        if len(children) > 1:
            return children[1]
        else:
            return None
    
    #---------------------------------------------------------------#
    
    def getLastComponentChild(self, assessment = None):
        """
            Returns the last ordered child in the container.
            (optionally using the assessment for
            dynamically controlled children)
            
            RETURNS: None if the aren't any ordered children or if the 
            container isn't ordered
        """
        
        #get the children
        children = self.getRandomizedChildren(assessment)
        
        #are there any children
        if len(children) == 0:            
            return None
        else:   
            return children[len(children)-1]
    
    #---------------------------------------------------------------#
    
    def getComponentSiblingBeforeChild(self, child, assessment = None):
        """
            Returns the child immediately before the specified child
            (optionally using the assessment for
            dynamically controlled children)
            
            RETURNS: None if the aren't any children before the specified
            child (or if the container isn't ordered)
        """
        
        #get the children (in reverse order)
        children = self.getRandomizedChildren(assessment)
        
        #are there any children?
        if len(children) > 0:
            children.reverse()
            
            #remove children that are before the specified child
            while len(children) > 0 and children[0].getIdentity() != child.getIdentity():
                del children[0]
                
        #return the child after the first (specified) child (which is the one we are after)
        if len(children) > 1:
            return children[1]
        else:
            return None
        
    #---------------------------------------------------------------#
    # BEGIN: Assessible Implementation                              #
    #---------------------------------------------------------------#
        
    def getAssessmentValueType(self):
        """
            ABSTRACT: Returns the type of the value that is to be stored
            for this component when it's assessment state is persisted.
            
            NOTE 1: See AssessmentComponentValueTypes for a list of defined contants       
            NOTE 2: This method MUST be overriden in the implementing
            class to specify the correct type for value persistence
            
            RETURN TYPE: String
        """
        
        #randomizedsection assessment values are strings
        return st_STRING
        
    #---------------------------------------------------------------#
        
    def getDefaultAssessmentState(self, assessment):
        """
            ABSTRACT: Constructs and returns a suitable 'default' 
            AssessmentComponentState for the provided Assessment
            instance.
            NOTE: This method MUST be overriden in the implementing
            class to provide a suitable default value for the component
            
            RETURN TYPE: AssessmentComponentState
        """
        
        return AssessmentComponentState(assessment.getIdentity(), assessment.getModelIdentity(), self.getIdentity(), self.getAssessmentValueType(), "[]", revision=assessment.getRevision())
            
    #---------------------------------------------------------------#
    
    def updateAssessmentState(self, assessment, formValue):
        """
            ABSTRACT: Updates the specified assessment using
            the provided HTTP REQUEST formValue.
            
            NOTE: This method MUST be overriden in the implementing
            class to suitably update the component state
        """
            
        #get the current assessment state for the component from the assessment
        state = assessment.getComponentState(self.getIdentity())
        
        #update the current state
        state.setValue(formValue)
        
        #update the assessment
        assessment.setIsModified(true)
            
    #---------------------------------------------------------------#
    # END: Assessible Implementation                                #
    #---------------------------------------------------------------#
    
    #---------------------------------------------------------------#
    # BEGIN: KERLExtensions Implementation                          #
    #---------------------------------------------------------------#

    def compileKERLtoJavaScript(self, expression):
        """
            Returns a Python string that represents a JavaScript
            expression, which may be used to evaluate the status
            of the component class specific KERL expression.

            NOTE: this method is primarily used to produce
            JavaScript expressions that form the basis of the
            client-side showif technology.
            
            RETURN TYPE: String         
        """
        
        #example: cond="['randsec1','haschosen', 'compid']" returns 'document.getElementById("randsec1:string").value.indexOf('compid')>=0'
        #example: cond="['randsec1','hasntchosen', 'compid']" returns 'document.getElementById("randsec1:string").value.indexOf('compid')<0'
        #get the identity (which is the identity of itself)        
        identity = self.getIdentity()
        
        #get the operator
        opr = expression[1]
        
        #get the value
        value = expression[2]
        
        #determine the result
        result = "document.getElementById(\"" + identity + ":string\").value"
        
        if opr == "contains":
            return result + ".indexOf(\"" + value + "\")>=0"
        
        elif opr == "doesntcontain":
            return result + ".indexOf(\"" + value + "\")<0"
        
        else:
            return "true"

    #---------------------------------------------------------------#

    def compileKERLtoEnglish(self, expression):
        """
            Returns a string representing an 'English' version
            of the component class specific KERL expression.

            NOTE: this method is primarily used to produce
            English descriptions of KERL expressions that may be 
            used to describe things like AssessmentFilters
            
            RETURN TYPE: String         
        """
        
        return ""

    #---------------------------------------------------------------#

    def evaluate(self, expression, assessment):
        """
            Evalutes the component class specific KERL expression
            using the provided assessment.

            NOTE: this method is primarily used to produce
            JavaScript expressions that form the basis of the
            client-side showif technology.

            RETURN TYPE: Boolean         
        """
        
        #get the identity
        identity = self.getIdentity()
        
        #get the operator
        opr = expression[1]
        
        #get the value
        value = expression[2]
        
        #get the stored state
        state = assessment.getComponentState(identity)
        
        #evaluate the expression
        if opr == "haschosen":
            return state.getValue().find(value) >= 0
        
        elif opr == "hasntchosen":
            return state.getValue().find(value) == -1
        
        else:
            return true
    
    #---------------------------------------------------------------#

    def getDependencies(self, expression):
        """
            Returns a list of components that are required to
            evaluate the component class specific KERL expression.

            NOTE: Includes the specific component itself.

            RETURN TYPE: {Component} indexed by Component.getIdentity()
        """

        referencedComponent = self.getModel().getComponent(expression[2])
        if referencedComponent is not None:
            return {self.getIdentity():self, referencedComponent.getIdentity():referencedComponent}            
        else:
            return {self.getIdentity():self}
    
    #---------------------------------------------------------------#

    def updateKERLIdentities(self, expression, identityMappings):
        """
            Returns the expression with the appropriate old identities
            (as identified by the identityMappings) replaced with 
            new identities.
        """
        
        #get the component identity from the expression (first element)
        identity = expression[0]
        
        #is the component in the identityMappings?
        if identityMappings.has_key(identity):
            expression[0] = identityMappings[identity]
        
        #also map the containing id
        if identityMappings.has_key(expression[2]):
            expression[2] = identityMappings[expression[2]]
       
        return expression

    #---------------------------------------------------------------#
    # END: KERLExtensions Implementation                            #
    #---------------------------------------------------------------#
        
#####################################################################
## Class Initialisation
# Register Component Information with the Knowledge Engine
registerGenericObjectClass(RandomizedSection)
#####################################################################
